1. React.memo

当父组件被重新渲染的时候,也会触发子组件的重新渲染,这样就多出了无意义的性能开销。如果子组件的状态没有发生变化,则子组件是不需要被重新渲染的。

我们可以使用 React.memo 来解决上述的问题,从而达到提高性能的目的。

注意:React.memo不是hooks,而是react的api。

image-20231128114600357

React.memo 的语法格式如下:

例如,在下面的代码中,父组件声明了 countflag 两个状态,子组件依赖于父组件通过 props 传递的 num。当父组件修改 flag 的值时,会导致子组件的重新渲染:

我们使用 React.memo(函数式组件) 将子组件包裹起来,只有子组件依赖的 props 发生变化的时候,才会触发子组件的重新渲染。示例代码如下:

也可以直接从react导入类型和方法,简写成这样:

 

2. useMemo

1. 问题引入

进一步改造前面的案例:我们希望在 Father 组件中添加一个“计算属性”,根据 flag 值的真假,动态返回一段文本内容,并把计算的结果显示到页面上。示例代码如下:

代码编写完毕后,我们点击父组件中的 +1 按钮,发现 count 在自增,而 flag 的值不会发生变化。此时也会触发 tips 函数的重新执行,这就造成了性能的浪费。我们希望如果 flag 没有发生变化,则避免 tips 函数的重新计算,从而优化性能。此时需要用到 React Hooks 提供的 useMemo API。

2. useMemo 的语法格式

useMemo 的语法格式如下:

其中:

  1. cb:这是一个函数,用于处理计算的逻辑,必须使用 return 返回计算的结果

  2. array:这个数组中存储的是依赖项,只要依赖项发生变化,都会触发 cb 的重新执行。使用 array 需要注意以下几点

    • 不传数组,则每次更新都会重新计算
    • 空数组,则只会计算一次
    • 依赖对应的值,则对应的值发生变化时会重新执行 cb

3. 使用 useMemo 解决刚才的问题

  1. 导入 useMemo:

  2. 在 Father 组件中,使用 useMemotips 进行改造:

  3. 此时,点击 Father 中的 +1 按钮,并不会触发 tips 的重新计算,而是会使用上一次缓存的值进行渲染。只有依赖项 flag 变化时,才会触发 tips 的重新计算。

3. useCallback

语法格式

之前我们所学的 useMemo 能够达到缓存某个变量值的效果,而当前要学习的 useCallback 用来对组件内的函数进行缓存,它返回的是缓存的函数。它的语法格式如下:

useCallback 会返回一个 memorized 回调函数供组件使用,从而防止组件每次 rerender 时反复创建相同的函数,能够节省内存开销,提高性能。其中:

  1. cb 是一个函数,用于处理业务逻辑,这个 cb 就是需要被缓存的函数

  2. array 是依赖项列表,当 array 中的依赖项变化时才会重新执行 useCallback。

    • 如果省略 array,则每次更新都会重新计算
    • 如果 array 为空数组,则只会在组件第一次初始化的时候计算一次
    • 如果 array 不为空数组,则只有当依赖项的值变化时,才会重新计算

基本示例

接下来,我们通过下面的例子演示使用 useCallback 的必要性:当输入框触发 onChange 事件时,会给 kw 重新赋值。kw 值的改变会导致组件的 rerender,而组件的 rerender 会导致反复创建 onKwChange 函数并添加到 Set 集合中,造成了不必要的内存浪费。代码如下:

运行上面的代码,我们发现每次文本框的值发生变化,都会打印 set.size 的值,而且这个值一直在自增 +1,因为每次组件 rerender 都会创建一个新的 onKwChange 函数添加到 set 集合中。

为了防止 Search 组件 rerender 时每次都会重新创建 onKwChange 函数,我们可以使用 useCallback 对这个函数进行缓存。改造后的代码如下:

运行改造后的代码,我们发现无论 input 的值如何发生变化,每次打印的 set.size 的值都是 1。证明我们使用 useCallback 实现了对函数的缓存。

另外一个对于input的onChange绑定事件的优化,就是直接在onChange属性里面写函数,这样应该不会生成多个函数,可以试一试。

useCallback 的案例

1. 问题引入

  1. 导入需要的 hooks 函数,并定义需要的 TS 类型:

  2. 定义 SearchInput 搜索框子组件,接收父组件传递进来的 onChange 处理函数,每当 input 触发 onChange 事件时,调用 props.onChange 进行处理:

  3. 定义 SearchResult 搜索结果子组件,接收父组件传递进来的 query 搜索关键字,在 useEffect 中监听 props.query 的变化,从而请求搜索的结果:

  4. 定义父组件 SearchBox 并渲染 SearchInput 组件和 SearchResult 组件。在父组件中监听 SearchInputonChange 事件,并把父组件中定义的处理函数 onKwChange 传递进去。同时,把父组件中定义的搜索关键字 kw 传递给 SearchResult 组件。示例代码如下:

  5. 经过测试后,我们发现:

    SearchResult子组件是需要跟随kw的变化重新渲染的,这个不用管。这里重点关注的是SearchInput子组件的更新过程。

    1. 每当SearchInput子组件的文本框内容发生变化,都会调用 props.onChange 把数据发送给父组件。
    2. 相应的,父组件通过 onKwChange 函数可以获取到SearchInput子组件的值,并把值更新到 kw 中。当 kw 发生变化,会触发父组件的 rerender。
  6. 而父组件的 rerender 又会重新生成 onKwChange 函数并把函数的引用作为 props 传递给SearchInput子组件。

    1. 这样,SearchInput子组件就监听到了 props 的变化,最终导致SearchInput子组件的 rerender。

    其实,SearchInput子组件根本不需要被重新渲染,因为 props.onChange 函数的处理逻辑没有发生变化,只是它的引用每次都在变。为了解决这个问题,我们需要用到 useCallbackReact.memo

2. 问题解决

  1. 首先,我们需要让子组件 SearchInput 被缓存,所以我们需要使用 React.memo 对其进行改造:

  2. 使用 React.memo 对组件进行缓存后,如果子组件的 props 在两次更新前后没有任何变化,则被 memo 的组件不会 rerender。

    所以为了实现 SearchInput 的缓存,还需要基于 useCallback 把父组件传递进来的 onChange 进行缓存。

    在父组件中针对 onKwChange 调用 useCallback,示例代码如下:

  3. 经过测试,我们发现每当文本框内容发生变化,不会导致 SearchInput 组件的 rerender。

思路要理清楚,首先是onKwChange函数只需要生成一个,所以用到了useCallback方法,然后是SearchInput组件里面,父组件传递过来的属性onChange并没有发生变化,所以不需要重新渲染,于是用到了React.memo。

这套组合拳打的很经典,要灵活运用。

4. useTransition

1. 问题引入

useTransition 可以将一个更新转为低优先级更新,使其可以被打断不阻塞 UI 对用户操作的响应,能够提高用户的使用体验。它常用于优化视图切换时的用户体验。

例如有以下3个标签页组件,分别是 HomeMovieAbout,其中 Movie 是一个渲染特别耗时的组件,在渲染 Movie 组件期间页面的 UI 会被阻塞,用户会感觉页面十分卡顿,示例代码如下:

配套的 CSS 样式为:

当movieTab里面是简单的字符串时,可以很轻松的切换:

当movieTab里面是大数据时,可以看到不光UI没有渲染出来,连button切换也不能进行了:

2. 语法格式

因为在默认情况下,react会先执行渲染操作而不是响应用户的操作,所以当切换到Movie组件的时候,react会先渲染Movie组件,但是Movie组件渲染非常耗时,所以这时候用户想切换到其它组件,react也是不会响应的。这时候就要用到useTransition这个hook。

参数

返回值(数组):

3. 问题解决

修改 TabsContainer 组件,使用 useTransition 把点击按钮后为 activeTab 赋值的操作,标记为低优先级。此时 React 会优先响应用户对界面的其它操作,从而保证 UI 不被阻塞:

此时,点击 Movie 按钮后,状态的更新被标记为低优先级,About 按钮的 hover 效果点击操作都会被立即响应。

4. 使用 isPending 展示加载状态

为了能够使用 isPending 的状态为按钮添加 loading 效果,我们需要把 useTransition 的调用从 TabsContainer 组件中挪到 TabButton 组件中:

用老师提供的代码,可以看到isPending起作用了,按钮的内容发生了变化:

但是我的代码中,isPending没有起作用,界面没有按照预想发生变化,为什么呢?

在Movie组件中,将父组件中传递过来的isPending输出,结果是false。说明刚开始渲染的时候,useTransition并没有将isPending设置为true,只有等到遇到后面的代码之后,才判断出要将isPending设置为true,而这个时候,Movie组件处在渲染过程中,那么isPending判断的那段代码是永远不会执行的。

可以看到,在点击movie按钮时,传递过来的isPending是false,切换到别的tab时,isPending的值是true,说明只有遇到费时的渲染代码时,react才会将isPending设置为true。

为什么老师代码里面可以使用isPending呢?因为useTransition定义在button组件中,在点击button时,绑定了startTransition事件,这样会触发父组件的setActiveTab,会造成父组件的重新渲染,里面的子组件也会重新渲染,此时定义在button里面的isPending就被设置为true了,所以可以正常使用。

那么我这样说看可不可以:isPending只能用在当前组件中,如果通过props传递,会有状态的延迟而达不到效果。

 

那如果就是想在Movie组件里面设置loading界面,应该怎么做呢?我想的话,应该不能将isPending状态定义在父组件中,这样传递给子组件的值就没有时间进行更改,应该定义在子组件中,然后传递给父组件,再通过父组件传递给另外的需要用到isPending的子组件。这个方法行不通。为什么呢?还是isPending值不是最新的问题,只有再次进入到button组件中,isPending才是最新值,此时该怎么执行组件的事件呢?我刚才是在onClick的时候传值的,现在呢?该怎么传值?

上面这种写法更加复杂,从button中获取pending状态,再传递到Movie组件中,是最新值吗?

 

我看视频里面老师是这么做的,在TabsContainer组件里面定义专门的渲染组件的函数:

效果非常好:

仔细看老师的代码,和我的代码有什么不同?我在上面有一段代码{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""},老师是先判断isPending的,然后再渲染具体的组件,我是先判断应该渲染哪个组件,然后判断isPending,先将我的代码按照老师的顺序测试一下,看效果。

为什么我的这种判断方式不行呢?尝试输出一下isPending和active的值,看一下到底是怎么变化的:

image-20240219142738936

可以看到isPending为true时,active的状态是原来的状态,isPending为false了,active才会变为最新值。那么按照我的判断条件:{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""},active变为movie了,isPending就为false了,loading状态只有在isPending从false变为true再变为false时才会显示,从上面可以看出,这个时间非常短,根本达不到loading的需求。

而renderTabs()方法里面,当isPending变为true之后,页面渲染的就是loading...,后来isPending变为了false,就会渲染耗时的组件,而直到这个组件真正渲染到页面上之前,原来的界面loading...是不会注销的,就相当于间接达到了目的。

这整个流程一定要搞清楚,我根本没有想到耗时组件渲染之前的情况是什么样的,考虑到这一点就好理解了。

 

那么先判断isPending,再渲染组件,写成这样行不行呢?

这样是用到了pending,但是在home切换到movie的时候,会有三个“数据请求中,请稍后”,因为此时三个表达式都满足条件,所以都会被渲染出来。还是要按照老师的,写一个函数出来,首先就判断isPending,然后再分别渲染组件。

image-20240219141107838

5. 注意事项

  1. 传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。

 

  1. 标记为 transition 的状态更新将被其他状态更新打断。例如在 transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作。
  2. transition 更新不能用于控制文本输入。

5. useDeferredValue

1. 问题引入

在搜索框案例中,SearchResult 组件会根据用户输入的关键字,循环生成大量的 p 标签,因此它是一个渲染比较耗时的组件。代码如下:

可以看到,刷新浏览器之后我就输入了,很久之后才有反应。渲染还是非常耗时的,组件对输入的每一种情况都给出了响应。但很可能用户需要的并不是每一种情况都给出响应,而是输出一段话之后,根据这段话响应就行了。

注意,此案例不能使用 useTransition 进行性能优化,因为 useTransition 会把状态更新标记为低优先级被标记为 transition 的状态更新将被其他状态更新打断。因此在高频率输入时,会导致中间的输入状态丢失的问题。比如说输入了123,在输入1会触发setKw(e.currentTarget.value)这个方法,但是这个状态更新是被startTransition包裹了,所以会异步执行,那么当用户很快输入2时,react会优先响应用户操作,这个状态更新会被UI更新打断,然后如果用户很快输入3,前面的更新也会被打断,最后输入框里面就可能只剩下3来显示了。

例如:

我快速输入了123456,结果界面值渲染了6。

这里有一个现象,就是input组件直接写在SearchBox里面的时候,是会产生输入丢失的现象,但是如果将输入框作为一个单独的组件,使用useTransition,并没有输入丢失的现象:

这是为什么呢?暂时不知道原因。

2. 语法格式

useDeferredValue 提供一个 state 的延迟版本,根据其返回的延迟的 state 能够推迟更新 UI 中的某一部分,从而达到性能优化的目的。语法格式如下:

useDeferredValue 的返回值为一个延迟版的状态

  1. 在组件首次渲染期间,返回值将与传入的值相同
  2. 在组件更新期间,React 将首先使用旧值重新渲染 UI 结构,这能够跳过某些复杂组件的 rerender,从而提高渲染效率。随后,React 将使用新值更新 deferredValue,并在后台使用新值重新渲染是一个低优先级的更新。这也意味着,如果在后台使用新值更新时 value 再次改变,它将打断那次更新。

3. 问题解决

按需导入 useDeferredValue 这个 hooks API,并基于它进行搜索功能的性能优化:

虽然渲染还是耗时,但是对用户的响应做到了只对最后的输入做出响应,这也是一种优化。

4. 表明内容已过时

kw 的值频繁更新时,deferredKw 的值会明显滞后,此时用户在页面上看到的列表数据并不是最新的,为了防止用户感到困惑,我们可以给内容添加 opacity 透明度,表明当前看到的内容已过时。示例代码如下:

效果还是蛮好的。

对这个教程中涉及到Typescript的地方,总结一下写法:

 

image-20231214171201906

props的类型有时候这样写,有时候写成React.FC<React.PropsWithChildren>,要分清楚情况,后面这种情况是因为在子组件里面使用了{props.children}这样的代码,好好总结一下。

-----2024-02-19解答:

这是泛型的使用,不要和泛型的定义搞混了,在React.FC这个类型中,定义了一个泛型参数P,而这个P是使用者自己来定义的类型,在使用泛型的时候,需要传递一个类型参数,无论是上面图中的React.FC<{isPending:boolean}>,还是React.FC<React.PropsWithChildren>,还是下面更复杂的:

其实都是在自定义泛型P,都是用在props中的。

泛型的定义和使用,有点眼花。

像上面定义input的onChange事件函数的时候,就是泛型的使用,而不是定义:

还有定义组件类型的时候,也是泛型的使用:

image-20240219092613649

那么定义泛型的时候,有什么不同的地方呢?现在我还说不上来,就现在心里把泛型的定义和使用分开来看待就行了。

&&符号的使用?

&&我只知道是一个逻辑判断运算符,表示“逻辑与”。但是用在react中是什么意思呢?经常看到这种用法:{isPending && "..."},这是什么意思?

这不是react专门的用法,而是JS的用法。

在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。我之前的用法只是两个boolean值来进行计算,比如if(isActive && data.length > 0){},所以没有想到会有这种效果。

image-20231215153717635

在react中,&&右边的expression可以是字符串,也可以是一个组件。

对象数组怎么定义类型

在学习typescript的时候,定义的数组都是基本类型,比如说:(number | srting)[]或者Array<number | string>,那复杂一点的对象数组该怎么定义类型呢?其实就是为数组加上对象类型就行了,比如说:

image-20240219105738299

useState里面的数据类型怎么定义?

在使用useState定义数据的时候,基本类型的数据还好说,直接定义,ts会自动为我们推导出类型,但是复杂一点的数据,比如说对象数组、对象等等,不给出类型直接定义,在使用的时候就会有红色波浪线的提示:

image-20240219111128145

定义的list是一个对象数组,里面有一个属性word,在渲染的时候会用到,由于没有定义list的类型,所以会报红色波浪线。

那么该怎么定义数据类型呢?

image-20240219110524312

从react官方的TS定义来看,useState需要传递一个泛型参数进去即可。

那么可以写成这样:

image-20240219111343155

这样就OK了。

使用函数返回值时怎么写ts

在使用函数返回值时,由于惯性的思维,想直接为这个变量定义类型,就像这样自然:

但是函数返回值有一点很不同的地方,就是ts里面的函数基本上已经定义好了参数类型和返回值类型,而且如果函数里面使用了泛型的话,那么调用函数的时候,只需要指定泛型类型即可,函数的参数和返回值都会用到泛型的类型,这样用起来就非常简洁了。

我这里记录一下我的一种操作,在定义zustand的store的时候,useStore是create函数的返回值,于是我经常在useStore的后面定义类型,可想而知这是不行的、错误的,我根本就不知道useStore的类型是什么,而不需要知道,只需要在create时候,传递泛型参数进去即可,create函数已经定义好了参数和返回值的类型,ts会帮助我们定义出useStore的类型,这里就不需要定义类型了。

如果有什么顾虑,可以直接看阮一峰的教程,函数一章。

在编写single-store的时候,是这样定义类型的,对于我这个typescript初学者来说,确实容易记混:

为什么这里是这样做呢?原因是这个返回值本质上是一个函数,实际上我们定义的是函数的类型,查看一下StateCreator的typescript原始定义:

可以看到类型定义主体是这个:

就是一个函数的类型定义。

实际上是没有看类型的原始定义、没有学透typescript造成的混乱。